/*
 * Galaxium Messenger
 * 
 * Copyright (C) 2005-2007 Ben Motmans <ben.motmans@gmail.com>
 * Copyright (C) 2005-2007 Philippe Durand <draekz@gmail.com>
 * Copyright (C) 2007-2008 Paul Burton <paulburton89@gmail.com>
 *
 * License: GNU General Public License (GPL)
 *
 * This program is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License as published by the Free
 * Software Foundation; either version 2 of the License, or (at your option)
 * any later version.
 *
 * This program is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
 * or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * for more details.
 *
 * You should have received a copy of the GNU General Public License along
 * with this program; if not, write to the Free Software Foundation, Inc.,
 * 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */

using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Security.Cryptography;
using System.Text;
using System.Timers;
using System.Web;
using System.Web.Services.Protocols;
using System.Xml;

using Anculus.Core;

using Galaxium.Client;
using Galaxium.Core;
using Galaxium.Protocol.Msn.Soap;
using Galaxium.Protocol.Msn.Soap.BodyParts;

namespace Galaxium.Protocol.Msn
{
	public sealed class NSConnection : CommandConnection
	{
		// Maximum OIMs to delete in one request
		const int _maxOIMDelete = 5;
		
		bool _isSynchronized;
		bool _initialDone = false;
		
		MsnProtocolVersion _ver = MsnProtocolVersion.CVR0;
		
		bool _allowDispatchConnect;
		
		Timer _pngTimer;
		uint _missedQNGs;
		
		Dictionary<Guid, OrderedDictionary<int, MsnOfflineTextMessage>> _offlineMessages = new Dictionary<Guid, OrderedDictionary<int, MsnOfflineTextMessage>> ();
		int _offlineMessagesToRetrieve;
		
		public new MsnSession Session
		{
			get { return base.Session as MsnSession; }
		}
		
		public MsnProtocolVersion Protocol
		{
			get { return _ver; }
		}
		
		internal NSConnection (MsnSession session, MsnNSConnectionInfo info)
			: base (session, info, MsnConnectionType.Notification)
		{
			// Setup the certificate policy to accept all.
			ServicePointManager.CertificatePolicy = new PassportCertificatePolicy ();
			//ServicePointManager.ServerCertificateValidationCallback += delegate { return true; };
			
			_allowDispatchConnect = (info.HostName != MsnConstants.DefaultNotificationServerHostname);
			
			_pngTimer = new Timer ();
			_pngTimer.Elapsed += PNGTimerElapsed;
		}
		
		public override void Dispose ()
		{
			_pngTimer.Dispose ();
			
			base.Dispose ();
		}
		
		protected override void OnErrorOccurred (object sender, ConnectionErrorEventArgs args)
		{
			if (_allowDispatchConnect)
			{
				// We only want to fall back to the dispatch server once
				_allowDispatchConnect = false;
				
				// Reset stored server to avoid using it again
				Session.Account.NotificationServerHostname = string.Empty;
				
				Connection.ConnectionInfo.HostName = MsnConstants.DefaultNotificationServerHostname;
				Connection.ConnectionInfo.Port = MsnConstants.DefaultNotificationServerPort;
				
				Reconnect();
			}
			else
				base.OnErrorOccurred (sender, args);
		}
		
		protected override void OnClosed (object sender, ConnectionEventArgs args)
		{
			lock (_pngTimer)
			{
				if (_pngTimer.Enabled)
					_pngTimer.Stop ();
			}
			
			base.OnClosed (sender, args);
			
			_ver = MsnProtocolVersion.CVR0;
		}
		
		protected override void OnAfterConnect (object sender, ConnectionEventArgs args)
		{
			base.OnAfterConnect (sender, args);
			
			string versions = string.Empty;
			
			foreach (string ver in Enum.GetNames (typeof (MsnProtocolVersion)))
				versions = ver + " " + versions;
			
			Send (new VERCommand (Session, versions.Trim ()));
		}
		
		private void ProcessDisplayImage (MsnContact contact, MsnDisplayImage obj)
		{
			if (obj != null)
			{
				if ((contact.DisplayImage == null) || ((contact.DisplayImage as MsnDisplayImage).Sha != obj.Sha))
				{
					//IDisplayImage oldImage = contact.DisplayImage;
					contact.SetUserDisplay (obj);
				}
			}
		}
		
#region Command Handlers
#pragma warning disable 169
		[CommandHandler]
		private void OnErrorReceived (ErrorCommand cmd)
		{
			Log.Error ("Error Occurred: {0}", cmd);
			
			if (MsnError.RequiresUserNotification (cmd.ErrorCode))
			{
				string description = MsnError.GetErrorDescription (cmd.ErrorCode);

				Session.EmitErrorOccurred (new ErrorEventArgs (Session, cmd.ErrorCode.ToString (), description));
			} //TODO: else: handle internally
		}
		
		[CommandHandler]
		void OnVERReceived (VERCommand cmd)
		{
			foreach (string ver in cmd.Versions)
			{
				try
				{
					_ver = (MsnProtocolVersion)Enum.Parse (typeof (MsnProtocolVersion), ver);
					break;
				}
				catch
				{
					_ver = MsnProtocolVersion.CVR0;
				}
			}
			
			if (_ver == MsnProtocolVersion.CVR0)
				throw new ApplicationException ("Notification server doesn't support any protocol versions we do");
			
			Log.Info ("Using MSN Protocol Version {0}", _ver);
			
			_allowDispatchConnect = false;
			
			Send (new CVRCommand (Session));
		}

		[CommandHandler]
		private void OnUBXReceived (UBXCommand msg)
		{
			MsnContact contact = msg.Contact as MsnContact;
			
			if (contact != null)
			{
				if (contact.CurrentMedia.ToString () != msg.CurrentMedia)
					contact.CurrentMedia = MsnCurrentMedia.FromString (msg.CurrentMedia);
				
				//string oldMessage = contact.DisplayMessage;
				
				if (contact.DisplayMessage != msg.PersonalMessage)
				{
					contact.DisplayMessage = msg.PersonalMessage;
				}
				
				Session.EmitContactChanged (new ContactEventArgs (contact));
			}
			else
			{
				Log.Warn("Contact is missing from collection!");
			}
		}
		
		[CommandHandler]
		private void OnILNReceived (ILNCommand cmd)
		{
			MsnContact contact = cmd.Contact;
			
			if (contact == null)
				return;
			
			ProcessDisplayImage (contact, cmd.DisplayImage);
			
			contact.ClientIdentifier = cmd.ClientIdentifier;
			
			if (contact.Presence != cmd.Presence)
				contact.Presence = cmd.Presence;
			
			if (string.IsNullOrEmpty (contact.DisplayName) || (!contact.DisplayName.Equals (cmd.DisplayName)))
				contact.SetDisplayName (cmd.DisplayName, true);
		}
		
		[CommandHandler]
		private void OnFLNReceived (FLNCommand cmd)
		{
			MsnContact contact = cmd.Contact as MsnContact;
			
			if (contact.Presence != MsnPresence.Offline)
			{
				IPresence oldpresence = contact.Presence;
				contact.Presence = MsnPresence.Offline;
				
				ActivityUtility.EmitActivity (this, new EntityPresenceChangeActivity (contact, oldpresence));
				
				Session.EmitContactPresenceChanged (new EntityChangeEventArgs<IPresence>(contact, MsnPresence.Offline, oldpresence));
				Session.EmitContactChanged (new ContactEventArgs (contact));
			}
		}
		
		[CommandHandler]
		private void OnGCFReceived (GCFCommand cmd)
		{
		}

		[CommandHandler]
		private void OnNLNReceived (NLNCommand cmd)
		{
			MsnContact contact = cmd.Contact;
			
			ProcessDisplayImage(contact, cmd.DisplayImage);
			
			contact.ClientIdentifier = cmd.ClientIdentifier;
			
			if(contact.Presence != cmd.Presence)
			{
				IPresence oldPresence = contact.Presence;
				contact.Presence = cmd.Presence;
				
				ActivityUtility.EmitActivity (this, new EntityPresenceChangeActivity (contact, oldPresence));
				
				Session.EmitContactPresenceChanged (new EntityChangeEventArgs<IPresence>(contact, contact.Presence, oldPresence));
			}
			
			if (!contact.DisplayName.Equals(cmd.DisplayName))
			{
				string oldName = contact.DisplayName;
				contact.DisplayName = cmd.DisplayName;
				
				ActivityUtility.EmitActivity (this, new EntityNameChangeActivity (contact, oldName));
				
				Session.EmitContactNameChanged (new EntityChangeEventArgs<string>(contact, contact.DisplayName, oldName));
			}
			
			Session.EmitContactChanged (new ContactEventArgs (contact));
		}

		[CommandHandler]
		private void OnRMLReceived (RMLCommand msg)
		{
			foreach (string key in msg.Items.Keys)
			{
				List<ListCommand.ListItem> list;
				
				if (msg.Items.TryGetValue(key, out list))
				{
					foreach (ListCommand.ListItem item in list)
					{
						MsnContact contact = Session.ContactCollection.GetContact (item.Contact+"@"+key) as MsnContact;
						
						if (contact != null)
						{
							contact.ListType &= ~item.ListType;
							
							Session.EmitContactChanged (new ContactEventArgs (contact));
						}
					}
				}
			}
		}
		
		[CommandHandler]
		private void OnADLReceived (ADLCommand cmd)
		{
			if (!_initialDone && cmd.IsOK)
			{
				_initialDone = true;
				
				CompleteLogin();
			}
			
			foreach (string key in cmd.Items.Keys)
			{
				List<ListCommand.ListItem> list;
				
				if (cmd.Items.TryGetValue(key, out list))
				{
					foreach (ListCommand.ListItem item in list)
					{
						MsnContact contact = Session.ContactCollection.GetContact (item.Contact.UniqueIdentifier) as MsnContact;
						
						if (contact != null)
						{
							contact.ListType |= item.ListType;
							
							switch (item.ListType)
							{
								case MsnListType.Pending:
								case MsnListType.Reverse:
									if (contact.IsInList(MsnListType.Forward))
										Session.EmitContactChanged (new ContactEventArgs (contact));
									else if (!contact.IsInList(MsnListType.Allowed) && !contact.IsInList (MsnListType.Blocked))
									{
										Session.EmitContactReverse (new ContactEventArgs (contact));
										ActivityUtility.EmitActivity (this, new NewContactActivity(this.Session, contact));
									}
									break;
								
								default:
									Session.EmitContactChanged (new ContactEventArgs (contact));
									break;
							}
						}
						else
						{
							Session.EmitContactReverse (new ContactEventArgs (item.Contact.UniqueIdentifier));
							ActivityUtility.EmitActivity (this, new NewContactActivity(this.Session, item.Contact.UniqueIdentifier));
						}
					}
				}
			}
		}
		
		[CommandHandler]
		private void OnXFRReceived (XFRCommand cmd)
		{
			if (!cmd.IsSwitchboard)
			{
				//reconnect notification server
				Session.Account.NotificationServerHostname = cmd.HostName;
				Session.Account.NotificationServerPort = cmd.Port;
				
				Connection.ConnectionInfo.HostName = cmd.HostName;
				Connection.ConnectionInfo.Port = cmd.Port;
				Reconnect();
			}
		}
		
		[CommandHandler]
		private void OnOUTReceived (OUTCommand cmd)
		{
			if (cmd.OTH)
			{
				// We have been booted off MSN from another client.
				Session.EmitUsurped (new SessionEventArgs (Session));
			}
		}
		
		[CommandHandler]
		private void OnRNGReceived (RNGCommand cmd)
		{
			SBConnection sb = Session.FindSwitchboard (cmd.SwitchboardIdentifier);
			
			if (sb == null)
			{
				sb = new SBConnection (Session, new MsnSBConnectionInfo (cmd.HostName, cmd.Port, Connection.ConnectionInfo.UseHTTP, cmd.AuthenticationString), cmd.AuthenticationString, cmd.SwitchboardIdentifier);
				Session._switchboards.Add (sb);
			}
			else
				sb.Connect (new MsnSBConnectionInfo (cmd.HostName, cmd.Port, Connection.ConnectionInfo.UseHTTP, cmd.AuthenticationString), cmd.AuthenticationString, cmd.SwitchboardIdentifier);
		}
		
		[CommandHandler]
		private void OnCVRReceived (CVRCommand cmd)
		{
			Session.EmitLoginProgress (new SessionProgressEventArgs (Session, "Authenticating...", 0.33));
			
			Send (new NSUSRCommand (Session as MsnSession, true, Session.Account.UniqueIdentifier));
		}
		
		[ContentHandler]
		private void OnProfileReceived (ProfileContent content)
		{
			Session.EmitLoginProgress (new SessionProgressEventArgs (Session, "Synchronizing...", 0.66));
			
			try
			{
				// Retrieve Membership List
				// TODO: Which services can we actually make use of?
				
				ServiceFilter filter = new ServiceFilter ();
				filter.Types.Add (new ServiceType ("Messenger"));
				filter.Types.Add (new ServiceType ("Invitation"));
				filter.Types.Add (new ServiceType ("SocialNetwork"));
				filter.Types.Add (new ServiceType ("Space"));
				filter.Types.Add (new ServiceType ("Profile"));
			
				Session.SharingService.BeginFindMembership (filter, delegate (IAsyncResult memberAsyncResult)
				{
					ThreadUtility.Check ();
					
					try
					{
						FindMembershipResult membershipResult = Session.SharingService.EndFindMembership (memberAsyncResult);
						Session.UpdateMemberships (membershipResult);
					}
					catch (SoapException ex)
					{
						if ((ex.Detail != null) && (ex.Detail["errorcode"] != null) && (ex.Detail["errorcode"].InnerText == "ABDoesNotExist"))
						{
							// This is a brand new account, it doesn't have an address book yet
							
							ABInfo info = new ABInfo ();
							info.Name = null;
							info.OwnerPuid = 0;
							info.OwnerEmail = Session.Account.UniqueIdentifier;
							info.FDefault = true;
							
							//TODO: make this async
							Session.ABService.ABAdd (info);
							
							Session.UpdateMemberships (new FindMembershipResult ());
						}
						else
							throw ex;
					}
					catch (Exception ex)
					{
						Log.Warn (ex, "Error Requesting Lists");
						
						Session.EmitErrorOccurred (new ErrorEventArgs (Session, "Service Unavailable", "Service Unavailable"));
						return;
					}
				
					// Retrieve Address Book
					Session.ABService.BeginABFindAll (new Guid (), ABView.Full, false, new DateTime (1, 1, 1), delegate (IAsyncResult abAsyncResult)
					{
						ThreadUtility.Check ();
						
						try
						{
							ABFindAllResult abResult = Session.ABService.EndABFindAll (abAsyncResult);
							Session.UpdateAddressBook (abResult);
						}
						catch (Exception ex)
						{
							Log.Warn (ex, "Error Requesting Lists");
							
							Session.EmitErrorOccurred (new ErrorEventArgs (Session, "Service Unavailable", "Service Unavailable"));
							return;
						}
			
						// Set privacy settings
						MsnAccount account = Session.Account as MsnAccount;
						SetPrivacy (account.AllowUnknownContacts);
					}, null);
				}, null);
			}
			catch (Exception ex)
			{
				Log.Warn (ex, "Error Requesting Lists");
				
				Session.EmitErrorOccurred (new ErrorEventArgs (Session, "Service Unavailable", "Service Unavailable"));
				return;
			}
		}
		
		[CommandHandler]
		private void OnBLPReceived (BLPCommand cmd)
		{
			if (!_isSynchronized)
			{
				_isSynchronized = true;
				
				HandleContacts();
			}
		}

		[CommandHandler]
		private void OnUSRReceived (NSUSRCommand cmd)
		{
			if (cmd.IsOK)
			{
				Session.EmitLoginProgress (new SessionProgressEventArgs (Session, "Synchronizing...", 0.66));
				return;
			}
			
			BeginSSOAuthentication (cmd);
		}
		
		[CommandHandler]
		private void OnCHLReceived (CHLCommand cmd)
		{
			Send (new QRYCommand (Session as MsnSession, MsnConstants.ProductID, new Challenge (MsnConstants.ProductID, MsnConstants.ProductKey).GetChallengeResponse (cmd.Challenge)));
		}
		
		[ContentHandler]
		void OIMNotificationContentReceived (OIMNotificationContent content)
		{
			ProcessMailData (content.MailData);
		}
		
		[CommandHandler]
		void OnSBSReceived (SBSCommand cmd)
		{
			
		}
		
		[ContentHandler]
		void OnInitialMailReceived (InitialEmailContent content)
		{
			ProcessMailData (content.MailData);
		}
		
		[CommandHandler]
		void OnCHGReceived (CHGCommand cmd)
		{
			IPresence old_presence = Session.Account.Presence;
			Session.Account.SetPresence (cmd.Presence);
			Session.Account.EmitPresenceChange (new EntityChangeEventArgs<IPresence> (Session.Account, cmd.Presence, old_presence));
		}
		
		[CommandHandler]
		void OnUUXReceived (UUXCommand cmd)
		{
		}
		
		[CommandHandler]
		void OnQRYReceived (QRYCommand cmd)
		{
			
		}
		
		[CommandHandler]
		void OnUBNCommand (UBNCommand cmd)
		{
			Log.Info ("Received UBN Command\n{0}", Encoding.UTF8.GetString (cmd.ToByteArray ()));
		}
		
		[CommandHandler]
		void OnUUNReceived (UUNCommand cmd)
		{
			
		}
		
		[CommandHandler]
		void OnUBMCommand (UBMCommand cmd)
		{
			if ((cmd.Type == UUMType.TextMessage) || (cmd.Type == UUMType.TypingUser) || (cmd.Type == UUMType.Nudge))
			{
				MsnConversation conv = Session.Conversations.GetConversation (cmd.Source as MsnContact) as MsnConversation;
				
				if (conv == null)
				{
					conv = new MsnConversation (cmd.Source as MsnContact);
					Session.Conversations.Add (conv);
				}
				
				conv.ProcessContent (cmd.Content);
			}
			else
				Log.Info ("Received Unknown UBM Command\n{0}", Encoding.UTF8.GetString (cmd.ToByteArray ()));
		}
		
		[CommandHandler]
		void OnUUMReceived (UUMCommand cmd)
		{
			
		}
		
		[CommandHandler]
		void OnQNGReceived (QNGCommand cmd)
		{
			// Reset _missedQNGs (number of consecutively missed QNGs) to 0
			_missedQNGs = 0;
			
			// Reset _pngTimer with our new timeout
			lock (_pngTimer)
			{
				if (_pngTimer.Enabled)
					_pngTimer.Stop ();
				
				_pngTimer.Interval = cmd.Delay * 1000;
				
				_pngTimer.Start ();
			}
		}
		
		
		[CommandHandler]
		void OnNOTReceived (NOTCommand cmd)
		{
		}
#pragma warning restore 169
#endregion Command Handlers
		
		private void HandleContacts ()
		{
			int count = 0;
			int adlComplete = 0;
			
			lock (Session.ContactCollection)
			{
				foreach (MsnContact contact in Session.ContactCollection)
					if (contact.IsInList (MsnListType.Forward))
						adlComplete++;
				
				if (adlComplete == 0)
				{
					// There's no contacts to handle, so we don't need the ADL
					
					_initialDone = true;
					CompleteLogin();
				}
				else
				{
					ADLCommand adl = new ADLCommand (Session as MsnSession);
					
					foreach (MsnContact contact in Session.ContactCollection)
					{
						contact.Load();
						
						//only send contacts in FL
						if (contact.IsInList (MsnListType.Forward))
						{
							//contacts that are not in the list should not be sent (official client behavior)
							adl.Add (contact);
							
							count++;
							
							//ADL can contain max 150 items (~7500 bytes), we take a safer limit
							if ((count > 0 && (count % 125) == 0) || count >= adlComplete)
							{
								Send (adl);
								adl.Clear ();
							}
							
							if (count >= adlComplete)
								break;
						}
					}
				}
			}
		}
		
		private void CompleteLogin ()
		{
			MsnAccount account = Session.Account as MsnAccount;
			
			// Setup the account using its own data.
			Send (new PRPCommand (Session, account.DisplayName), delegate {});
			Send (new CHGCommand (Session, account.Presence, MsnClientIdentifier.Default, account.DisplayImage as MsnDisplayImage), delegate {});
			Send (new UUXCommand (Session, account.DisplayMessage, account.CurrentMedia), delegate {});
			
			// Obtain the roaming profile.
			Session.RequireSecurityTokens (new ExceptionDelegate (delegate
			{
				Log.Debug ("Requesting roaming profile");
				
				ProfileHandle handle = new ProfileHandle ();
				ProfileAttributes attributes = new ProfileAttributes ();
				
				handle.Alias = new ProfileAlias (Session._addressBook.AB.Info.OwnerCID.ToString (), "MyCidStuff");
				handle.RelationshipName = "MyProfile";
				
				Session.StoreService.BeginGetProfile (handle, attributes, ProcessRoamingProfile, null);
			}), SecurityToken.Storage);
			
			// Emit that the login sequence is completed.
			Session.EmitLoginCompleted (new SessionEventArgs (Session));
			
			// Start the PNG timer if we are not using HTTP method.
			lock (_pngTimer)
			{
				_pngTimer.Interval = 50000;
				
				if (!(Connection is HTTPConnection))
					_pngTimer.Start ();
			}
			
			// Check to see if we have any contacts that should be emitted the "new contact" activity.
			foreach (MsnContact contact in this.Session.ContactCollection)
			{
				// Look for contacts that have ONLY reverse list.
				
				if (contact.IsInList (MsnListType.Reverse) || contact.IsInList(MsnListType.Pending))
				{
					if (!contact.IsInList (MsnListType.Allowed) && !contact.IsInList (MsnListType.Blocked) && !contact.IsInList (MsnListType.Forward))
					{
						ActivityUtility.EmitActivity (this, new NewContactActivity(this.Session, contact));
						Session.EmitContactReverse (new ContactEventArgs (contact));
					}
				}
			}
		}
		
		public void SetPrivacy (bool allowUnknownContacts)
		{
			Send (new BLPCommand (Session as MsnSession, allowUnknownContacts));
		}
		
		public bool MoveContactToGroup (out string error, string passport, string fromGuid, string toGuid)
		{
			if (!AddContactToGroups (out error, passport, toGuid))
				return false;
			
			if (!RemoveContactFromGroups (out error, passport, fromGuid))
				return false;
			
			return true;
		}
		
		public bool AddContactToGroups (out string error, string passport, params string [] guids)
		{
			error = "None";
			
			MsnContact contact = Session.ContactCollection.GetContact (passport) as MsnContact;
			
			try
			{
				ABContact abContact = new ABContact ();
				abContact.Id = new Guid (contact.Guid);
				abContact.IdSpecified = true;
				
				ABContactCollection abContacts = new ABContactCollection ();
				abContacts.Add (abContact);
				
				GroupFilter groupFilter = new GroupFilter ();
				foreach (string guidStr in guids)
					groupFilter.GroupIDs.Add (new Guid (guidStr));
				
				Session.ABService.ABGroupContactAdd (new Guid (), groupFilter, abContacts, null);
			}
			catch (Exception ex)
			{
				error = ex.Message;
				return false;
			}
			
			foreach (string guid in guids)
			{
				MsnGroup group = Session.GroupCollection.GetGroup(guid) as MsnGroup;
				group.Add(contact);
				
				Session.EmitContactAdded(new ContactListEventArgs(contact, group));
			}
			
			MsnGroup groupless = Session.GroupCollection.GetGroup ("0") as MsnGroup;
			if (groupless.Contains (contact))
				groupless.Remove (contact);
			
			Session.EmitContactRemoved (new ContactListEventArgs(contact, groupless));
			
			return true;
		}
		
		public bool AddContactWithGroups(out string error, string passport, string alias, bool block, params string [] guids)
		{
			error = "None";
			
			MsnContact contact = Session.ContactCollection.GetContact (passport) as MsnContact;
			ABGroupContactAddResult result;
			
			try
			{
				ABContact abContact = new ABContact ();
				abContact.Info.PassportName = passport;
				abContact.Info.IsMessengerUser = true;
				abContact.Info.IsSmtp = false;
				abContact.Info.IsSmtpSpecified = true;
				abContact.InfoSpecified = true;
				
				if (!string.IsNullOrEmpty (alias))
				{
					abContact.Info.Annotations["AB.NickName"] = alias;
					abContact.Info.AnnotationsSpecified = true;
				}
				
				ABContactCollection abContacts = new ABContactCollection ();
				abContacts.Add (abContact);
				
				GroupFilter groupFilter = new GroupFilter ();
				foreach (string guidStr in guids)
					groupFilter.GroupIDs.Add (new Guid (guidStr));
				
				result = Session.ABService.ABGroupContactAdd (new Guid (), groupFilter, abContacts, new ABGroupContactAddOptions ());
			}
			catch (Exception ex)
			{
				error = ex.Message;
				return false;
			}
			
			if (contact == null)
			{
				contact = new MsnContact (Session, passport);
				
				lock (Session.ContactCollection)
					Session.ContactCollection.Add (contact);
			}
			
			contact.AddToLists (MsnListType.Forward | (block ? MsnListType.Blocked : MsnListType.Allowed));
			contact.Guid = result.Guid.ToString ();
			
			foreach (string guid in guids)
			{
				MsnGroup group = Session.GroupCollection.GetGroup(guid) as MsnGroup;
				group.Add(contact);
				
				Session.EmitContactAdded(new ContactListEventArgs(contact, group));
			}
			
			MsnGroup groupless1 = Session.GroupCollection.GetGroup ("0") as MsnGroup;
			groupless1.Remove (contact);
			
			Session.EmitContactRemoved (new ContactListEventArgs(contact, groupless1));
			
			return true;
		}
		
		public bool AddContact (out string error, string passport, string alias, bool block)
		{
			error = "None";
			
			MsnContact contact = Session.ContactCollection.GetContact (passport) as MsnContact;
			ABContactAddResult result;
			
			try
			{
				ABContact abContact = new ABContact ();
				abContact.Info.PassportName = passport;
				abContact.Info.IsMessengerUser = true;
				abContact.InfoSpecified = true;
				
				if (!string.IsNullOrEmpty (alias))
				{
					abContact.Info.MemberInfo.DisplayName = alias;
					abContact.Info.MemberInfoSpecified = true;
				}
				
				ABContactCollection abContacts = new ABContactCollection ();
				abContacts.Add (abContact);
				
				result = Session.ABService.ABContactAdd (new Guid (), abContacts, new ABContactAddOptions ());
			}
			catch (Exception ex)
			{
				error = ex.Message;
				return false;
			}

			if (contact == null)
			{
				contact = new MsnContact (Session, passport);
				
				lock (Session.ContactCollection)
					Session.ContactCollection.Add (contact);
			}
			
			contact.AddToLists (MsnListType.Forward | (block ? MsnListType.Blocked : MsnListType.Allowed));
			
			contact.Guid = result.Guid.ToString ();
			
			MsnGroup group1 = Session.GroupCollection.GetGroup ("0") as MsnGroup;
			group1.Add (contact);
			
			Session.EmitContactAdded (new ContactListEventArgs(contact, group1));
			
			return true;
		}
		
		public bool RemoveContactFromGroups (out string error, string passport, params string [] guids)
		{
			error = "None";
			MsnContact contact = Session.ContactCollection.GetContact (passport) as MsnContact;
			
			if (contact != null)
			{
				try
				{
					ABContact abContact = new ABContact ();
					abContact.Id = new Guid (contact.Guid);
					abContact.IdSpecified = true;
					
					ABContactCollection abContacts = new ABContactCollection ();
					abContacts.Add (abContact);
					
					GroupFilter groupFilter = new GroupFilter ();
					foreach (string guidStr in guids)
						groupFilter.GroupIDs.Add (new Guid (guidStr));
					
					Session.ABService.ABGroupContactDelete (new Guid (), abContacts, groupFilter);
				}
				catch (Exception ex)
				{
					error = ex.Message;
					return false;
				}
				
				foreach (string guid in guids)
				{
					MsnGroup group = Session.GroupCollection.GetGroup (guid) as MsnGroup;
					group.Remove (contact);
					Session.EmitContactRemoved (new ContactListEventArgs (contact, group));
				}
				
				// If the contact is no longer part of any group, we have to add him to the "0" group.
				bool found = false;
				foreach (MsnGroup group in Session.GroupCollection)
					if (group.Contains(contact))
						found = true;
				
				if (!found)
				{
					MsnGroup groupless = Session.GroupCollection.GetGroup ("0") as MsnGroup;
					groupless.Add (contact);
					Session.EmitContactAdded (new ContactListEventArgs(contact, groupless));
				}
				
				return true;
			}
			
			return false;
		}
		
		public bool RemoveContact (out string error, string passport)
		{
			error = "None";
			MsnContact contact = Session.ContactCollection.GetContact (passport) as MsnContact;
			
			if (contact != null && contact.Guid != null && contact.Guid != String.Empty)
			{
				try
				{
					ABContact abContact = new ABContact ();
					abContact.Id = new Guid (contact.Guid);
					abContact.IdSpecified = true;
					
					ABContactCollection abContacts = new ABContactCollection ();
					abContacts.Add (abContact);
					
					Session.ABService.ABContactDelete (new Guid (), abContacts);
				}
				catch (Exception ex)
				{
					error = ex.Message;
					return false;
				}
				
				contact.RemoveFromLists (MsnListType.Forward);
				
				foreach (MsnGroup group in Session.GroupCollection)
				{
					if (group.Contains (contact))
					{
						group.Remove (contact);
						Session.EmitContactRemoved (new ContactListEventArgs (contact, group));
					}
				}
				
				return true;
			}
			else
			{
				Log.Fatal ("Contact does not have a GUID to remove them from your list! Bad Internal Data!");
			}
			
			return false;
		}
		
		public bool AddGroup (out string error, string name)
		{
			error = "";
			
			ABGroupAddResult result;
			
			try
			{
				ABGroupAddInfo addInfo = new ABGroupAddInfo ();
				addInfo.GroupInfo.Name = name;
				addInfo.GroupInfo.Type = new Guid ("C8529CE2-6EAD-434d-881F-341E17DB3FF8");
				addInfo.GroupInfo.TypeSpecified = true;
				addInfo.GroupInfo.Messenger = false;
				addInfo.GroupInfo.MessengerSpecified = true;
				addInfo.GroupInfo.Annotations["MSN.IM.Display"] = "1";
				addInfo.GroupInfo.AnnotationsSpecified = true;
				
				result = Session.ABService.ABGroupAdd (new Guid (), new ABGroupAddOptions (), addInfo);
			}
			catch (Exception ex)
			{
				error = ex.Message;
				return false;
			}
			
			MsnGroup group = Session.GroupCollection.GetGroup (result.Guid.ToString ()) as MsnGroup;
			
			if (group == null)
				group = new MsnGroup (Session, result.Guid.ToString (), name, false);
			
			Session.GroupCollection.Add (group);
			
			Session.EmitGroupAdded (new GroupEventArgs (group));
			
			return true;
		}
		
		public bool RemoveGroup (out string error, string guid, bool clear)
		{
			error = "";
			
			MsnGroup group = Session.GroupCollection.GetGroup (guid) as MsnGroup;
			
			if (group != null)
			{
				List<string> uids = new List<string> ();
				
				foreach (MsnContact contact in group)
					uids.Add(contact.UniqueIdentifier);
				
				foreach (string uid in uids)
				{
					Log.Debug ("Removing uid from group: "+uid);
					
					MsnContact contact = group.GetContact (uid) as MsnContact;
					
					if (clear)
					{
						if (!RemoveContact (out error, contact.UniqueIdentifier))
							return false;
					}
					else
					{
						if (!RemoveContactFromGroups (out error, contact.UniqueIdentifier, group.UniqueIdentifier))
							return false;
					}
				}
				
				try
				{
					GroupFilter groupFilter = new GroupFilter ();
					groupFilter.GroupIDs.Add (new Guid (guid));
					
					Session.ABService.ABGroupDelete (new Guid (), groupFilter);
				}
				catch (Exception ex)
				{
					error = ex.Message;
					return false;
				}
				
				Session.GroupCollection.Remove (group);
				
				Session.EmitGroupRemoved (new GroupEventArgs (group));
				
				return true;
			}
			
			return false;
		}
		
		public bool RenameGroup (out string error, string guid, string name)
		{
			error = "";
			
			MsnGroup group = Session.GroupCollection.GetGroup(guid) as MsnGroup;
			
			if (group != null)
			{
				try
				{
					ABGroup abGroup = new ABGroup ();
					abGroup.Id = new Guid (guid);
					abGroup.IdSpecified = true;
					abGroup.Info.Name = name;
					abGroup.InfoSpecified = true;
					abGroup.PropertiesChanged = "GroupName";
					
					ABGroupCollection abGroups = new ABGroupCollection ();
					abGroups.Add (abGroup);
					
					Session.ABService.ABGroupUpdate (new Guid (), abGroups);
				}
				catch (Exception ex)
				{
					error = ex.Message;
					return false;
				}
				
				group.Name = name;
								
				Session.EmitGroupRenamed (new GroupEventArgs (group));
				
				return true;
			}
			
			return false;
		}
		
		void ProcessMailData (string data)
		{
			if (data == "too-large")
			{
				// The mail data was too large to fit into a command payload, so
				// we have to request it via SOAP
				
				RetrieveMailData ();
				return;
			}
			else if (string.IsNullOrEmpty (data))
				return;
			
			XmlDocument xml = new XmlDocument ();
			xml.LoadXml (data);
			
			List<XmlElement> oimElements = MsnXmlUtility.FindChildren (xml.DocumentElement, "M");
			
			if (oimElements.Count > 0)
			{
				Session.RequireSecurityTokens (new ExceptionDelegate (delegate
				{
					lock (_offlineMessages)
					{
						foreach (XmlElement oimElement in oimElements)
						{
							string id = MsnXmlUtility.FindChild (oimElement, "I").InnerText;
							string senderPassport = MsnXmlUtility.FindChild (oimElement, "E").InnerText;
							
							_offlineMessagesToRetrieve++;
							
							Session.OIMService.BeginGetMessage (id, false, OIMReceived, new KeyValuePair<string, string> (id, senderPassport));
						}
					}
				}), SecurityToken.Messenger);
			}
		}
		
		void OIMReceived (IAsyncResult asyncResult)
		{
			try
			{
				lock (_offlineMessages)
				{
					KeyValuePair<string, string> pair = (KeyValuePair<string, string>)asyncResult.AsyncState;
					
					string msgID = pair.Key;
					IMsnEntity source = Session.FindContact (pair.Value);
					
					MsnOfflineTextMessage msg = MsnOfflineTextMessage.FromMime (Session.OIMService.EndGetMessage (asyncResult));
					msg.Source = source;
					msg.ID = msgID;
					
					if (!_offlineMessages.ContainsKey (msg.RunID))
						_offlineMessages.Add (msg.RunID, new OrderedDictionary<int, MsnOfflineTextMessage> (true));
					
					_offlineMessages[msg.RunID].Add (msg.SequenceNum, msg);
					
					_offlineMessagesToRetrieve--;
					
					Log.Debug ("RunID {0}, waiting for {1}", msg.RunID, _offlineMessagesToRetrieve);
					
					if (_offlineMessagesToRetrieve == 0)
						OIMsAllReceived ();
				}
			}
			catch (Exception ex)
			{
				Log.Warn (ex, "Error retrieving OIM, retrying");
				
				// Try to retrieve the message again
				Session.RequireSecurityTokens (new ExceptionDelegate (delegate
				{
					lock (_offlineMessages)
					{
						KeyValuePair<string, string> pair = (KeyValuePair<string, string>)asyncResult.AsyncState;
						
						string id = pair.Key;
						string senderPassport = pair.Value;
						
						_offlineMessagesToRetrieve++;
						
						Session.OIMService.BeginGetMessage (id, false, OIMReceived, new KeyValuePair<string, string> (id, senderPassport));
					}
				}), SecurityToken.Messenger);
			}
		}
		
		void OIMsAllReceived ()
		{
			List<string> toDelete = new List<string> ();
			
			foreach (Guid runID in _offlineMessages.Keys)
			{
				MsnConversation conv = (Session.Conversations as MsnConversationManager).GetConversation (runID.ToString ());
				
				if (conv == null)
				{
					MsnContact src = null;
					
					//TODO: More efficient way to find the source?
					foreach (MsnOfflineTextMessage msg in _offlineMessages[runID].Values)
					{
						src = msg.Source as MsnContact;
						break;
					}
					
					// Check if we have a conversation with the source contact already
					conv = Session.Conversations.GetPrivateConversation (src) as MsnConversation;
					
					if (conv == null)
					{
						// We have no conversation with the source, create one
						
						conv = new MsnConversation (src, runID.ToString ());
						Session.Conversations.Add (conv);
					}
				}
				
				_offlineMessages[runID].Sort (delegate (KeyValuePair<int, MsnOfflineTextMessage> item1, KeyValuePair<int, MsnOfflineTextMessage> item2)
				{
					return item1.Key - item2.Key;
				});
				
				foreach (MsnOfflineTextMessage msg in _offlineMessages[runID].Values)
				{
					PlainTextContent content = new PlainTextContent (Session);
					content.Message = msg;
					
					conv.ProcessContent (content);
					
					toDelete.Add (msg.ID);
				}
			}
			
			_offlineMessages.Clear ();
			
			Log.Debug ("Ready to delete {0} offline messages", toDelete.Count);
			
			Session.RequireSecurityTokens (new ExceptionDelegate (delegate
			{
				while (toDelete.Count > 0)
				{
					List<string> thisDelete = new List<string> ();
					for (int i = 0; i < Math.Min (_maxOIMDelete, toDelete.Count); i++)
						thisDelete.Add (toDelete[i]);
					
					toDelete.RemoveRange (0, thisDelete.Count);
					
					Session.OIMService.BeginDeleteMessages (thisDelete.ToArray (), delegate (IAsyncResult asyncResult)
					{
						try
						{
							Session.OIMService.EndDeleteMessages (asyncResult);
							Log.Debug ("{0} offline messages deleted", thisDelete.Count);
						}
						catch (Exception ex)
						{
							Log.Error (ex, "Error deleting OIMs");
						}
					}, null);
				}
			}), SecurityToken.Messenger);
		}
		
		void RetrieveMailData ()
		{
			Session.RequireSecurityTokens (new ExceptionDelegate (delegate
			{
				Session.OIMService.BeginGetMetadata (OIMMetadataRetrieved, null);
			}), SecurityToken.Messenger);
		}
		
		void OIMMetadataRetrieved (IAsyncResult asyncResult)
		{
			string mailData = Session.OIMService.EndGetMetadata (asyncResult);
			ProcessMailData (mailData);
		}
		
		void PNGTimerElapsed (object sender, ElapsedEventArgs args)
		{
			if (_missedQNGs > 0)
				Log.Debug ("Missed {0} QNGs", _missedQNGs);
			
			//TODO: how many should we allow to miss in a row?
			if (_missedQNGs >= 3)
			{
				Log.Warn ("Too many QNGs missed, disconnecting");
				
				Connection.Disconnect ();
				return;
			}
			
			_missedQNGs++;
			
			ThreadUtility.SyncDispatch (new VoidDelegate (delegate
			{
				Send (new PNGCommand (Session));
			}));
		}
		
		void ProcessRoamingProfile (IAsyncResult asyncResult)
		{
			GetProfileResult profile = Session.StoreService.EndGetProfile (asyncResult);
			
			Session.Account._profile = profile;
			
			if (!string.IsNullOrEmpty (profile.ExpressionProfile.DisplayName))
				Session.Account.DisplayName = profile.ExpressionProfile.DisplayName;
			
			if (!string.IsNullOrEmpty (profile.ExpressionProfile.PersonalStatus))
				Session.Account.DisplayMessage = profile.ExpressionProfile.PersonalStatus;
			
			if (profile.ExpressionProfile.Photo != null)
			{
				//Log.Debug ("A photo is stored in the roaming profile");
				
				Session.RequireSecurityTokens (new ExceptionDelegate (delegate
				{
					foreach (DocumentStream docStream in profile.ExpressionProfile.Photo.DocumentStreams)
					{
						string uri = string.Format ("http://byfiles.storage.msn.com{0}?t={1}", docStream.PreAuthURL, Session.SecurityTokens[SecurityToken.Storage].RequestedSecurityToken.BinarySecurityToken.Ticket);

						Log.Debug ("Found photo document stream, uri {0}", uri);
						
						HttpWebRequest req = WebRequest.Create (uri) as HttpWebRequest;
						req.Timeout = 30000;
						
						req.Headers.Clear ();
						req.Accept = "*/*";
						req.Headers["Proxy-Connection"] = "Keep-Alive";
						req.KeepAlive = true;
						
						req.BeginGetResponse (delegate (IAsyncResult downloadAsyncResult)
						{
							try
							{
								WebResponse response = req.EndGetResponse (downloadAsyncResult);
								Stream stream  = response.GetResponseStream ();
								
								byte[] buf = new byte[256];
								byte[] data = new byte[response.ContentLength];
								int read;
								int written = 0;
								
								while ((read = stream.Read (buf, 0, buf.Length)) > 0)
								{
									Array.Copy (buf, 0, data, written, read);
									written += read;
									
									//Log.Debug ("Downloaded {0} of {1} bytes", written, data.Length);
								}
								
								ThreadUtility.SyncDispatch (new VoidDelegate (delegate
								{
									MsnDisplayImage image = new MsnDisplayImage (Session);
									image.Data = data;
									Session.Account.DisplayImage = image;
								}));
							}
							catch (Exception ex)
							{
								Log.Error (ex, "Error downloading display image");
							}
						}, null);
						
						break;
					}
				}), SecurityToken.Storage);
			}
			//else
			//	Log.Debug ("No photo stored in the roaming profile");
		}
		
		void BeginSSOAuthentication (NSUSRCommand cmd)
		{
			try
			{
				string policy = cmd.GetAuthData (0);
				string nonce = cmd.GetAuthData (1);
				
				SecurityToken.RegisterToken (SecurityToken.MessengerClear, policy);
				
				Session.RequireSecurityTokens (new ExceptionDelegate (delegate (Exception ex)
				{
					ThreadUtility.Check ();
					
					if (ex != null)
					{
						Log.Error (ex, "Unable to authenticate");
						Session.EmitErrorOccurred (new ErrorEventArgs (Session, "Authentication error", "Unable to authenticate."));
						Disconnect ();
					}
					
					string secToken = Session.SecurityTokens[SecurityToken.MessengerClear].RequestedSecurityToken.BinarySecurityToken.Token;
					string binSecret = Session.SecurityTokens[SecurityToken.MessengerClear].RequestedProofToken.BinarySecret;
					
					Send (new NSUSRCommand (Session as MsnSession, false, secToken, CalculateMBIResponse (binSecret, nonce)));
					
				}), SecurityToken.MessengerClear, SecurityToken.MessengerSecure, SecurityToken.Contacts);
			}
			catch (Exception ex)
			{
				Log.Error (ex, "Unable to authenticate");
				Session.EmitErrorOccurred (new ErrorEventArgs (Session, "Authentication error", "Unable to authenticate."));
				Disconnect ();
			}
		}
		
		byte[] Join (byte[] b1, byte[] b2)
		{
			byte[] ret = new byte[b1.Length + b2.Length];
			
			Array.Copy (b1, ret, b1.Length);
			Array.Copy (b2, 0, ret, b1.Length, b2.Length);
			
			return ret;
		}
		
		byte[] DeriveMBIKey (byte[] key, string magic)
		{
			HMACSHA1 sha = new HMACSHA1 ();
			sha.Key = key;
			
			byte[] magicb = Encoding.ASCII.GetBytes (magic);
			
			byte[] hash1 = sha.ComputeHash (magicb);
			byte[] hash2 = sha.ComputeHash (Join (hash1, magicb));
			byte[] hash3 = sha.ComputeHash (hash1);
			byte[] hash4 = sha.ComputeHash (Join (hash3, magicb));
			
			byte[] ret = new byte[hash2.Length + 4];
			Array.Copy (hash2, ret, hash2.Length);
			Array.Copy (hash4, 0, ret, hash2.Length, 4);
			
			return ret;
		}
		
		string CalculateMBIResponse (string key, string nonce)
		{
			byte[] key1 = Convert.FromBase64String (key);
			byte[] key2 = DeriveMBIKey (key1, "WS-SecureConversationSESSION KEY HASH");
			byte[] key3 = DeriveMBIKey (key1, "WS-SecureConversationSESSION KEY ENCRYPTION");
			
			HMACSHA1 sha = new HMACSHA1 ();
			sha.Key = key2;
			
			byte[] hash = sha.ComputeHash (Encoding.ASCII.GetBytes (nonce));
			
			byte[] iv = new byte[8];
			new Random ().NextBytes (iv);
			
			TripleDESCryptoServiceProvider des3 = new TripleDESCryptoServiceProvider ();
			des3.Key = key3;
			des3.Mode = CipherMode.CBC;
			des3.IV = iv;
			
			ICryptoTransform akm = des3.CreateEncryptor ();
			byte[] input = Encoding.ASCII.GetBytes (nonce + (char)8 + (char)8 + (char)8 + (char)8 + (char)8 + (char)8 + (char)8 + (char)8);
			byte[] output = new byte[72];
			akm.TransformBlock (input, 0, input.Length, output, 0);
			
			byte[] blob = new byte[28 + iv.Length + hash.Length + output.Length];
			MemoryStream stream = new MemoryStream (blob);
			BinaryWriter writer = new BinaryWriter (stream);
			
			writer.Write (BitUtility.FromInt32 (28, true));
			writer.Write (BitUtility.FromInt32 (1, true));              //CRYPT_MODE_CBC
			writer.Write (BitUtility.FromInt32 (26115, true));          //CALC_3DES
			writer.Write (BitUtility.FromInt32 (32772, true));          //CALG_SHA1
			writer.Write (BitUtility.FromInt32 (iv.Length, true));
			writer.Write (BitUtility.FromInt32 (hash.Length, true));
			writer.Write (BitUtility.FromInt32 (output.Length, true));
			writer.Write (iv);
			writer.Write (hash);
			writer.Write (output);
			
			writer.Close ();
			stream.Close ();
			
			return Convert.ToBase64String (blob);
		}
	}
}